"""
@author: 江同学呀
@file: geo_queries.py
@date: 2025/1/12 14:47
@desc:
    地理位置查询
    https://www.elastic.co/guide/en/elasticsearch/reference/7.17/geo-queries.html

    Elasticsearch supports two types of geo data: geo_point fields which support lat/lon pairs, and geo_shape fields,
    which support points, lines, circles, polygons, multi-polygons, etc.
    Elasticsearch 支持两种类型的地理数据：geo_point 字段（支持纬度/经度对）和 geo_shape 字段（支持点、线、圆、多边形、多多边形等）。
"""
from typing import Union, Dict, Literal, Iterable, Tuple

from espc.common.common import Number
from espc.common.query_common import DistanceType, ValidationMethod, SpatialRelation
from espc.orm.model.struct.geo import Point, PointType, Bounds
from espc.orm.model.dsl.queries.base_queries import _BaseQueries
from espc.orm.model.mapping.field.base_field.base_field import _BaseField


class _BaseGeoQueries(_BaseQueries, Bounds):
    """
    Geo查询基类

    :param field:
    :param _name:
        Optional name field to identify the filter
        用于标识筛选器的可选 name 字段
    :param validation_method:
        Set to IGNORE_MALFORMED to accept geo points with invalid latitude or longitude, set to COERCE to also try to
        infer correct latitude or longitude. (default is STRICT).
        设置为 IGNORE_MALFORMED 以接受纬度或经度无效的地理点，设置为 COERCE 以同时尝试推断正确的纬度或经度。（默认值为 STRICT）。
    :param ignore_unmapped:
        When set to true the ignore_unmapped option will ignore an unmapped field and will not match any documents for
        this query. This can be useful when querying multiple indexes which might have different mappings. When set to
        false (the default value) the query will throw an exception if the field is not mapped.
        当设置为 true 时，ignore_unmapped 选项将忽略未映射的字段，并且不会匹配此查询的任何文档。这在查询可能具有不同映射的多个索引时非常有用。
        当设置为 false （默认值） 时，如果字段未映射，则查询将引发异常。
    """
    _points: Iterable[PointType]

    def __init__(
            self, field: Union[str, _BaseField], _name: str = None, ignore_unmapped: bool = None,
            validation_method: Union[Literal["STRICT", "IGNORE_MALFORMED", "COERCE"], ValidationMethod] = None, *args,
            **kwargs
    ):
        super().__init__(**kwargs)
        self._field: Union[str, _BaseField] = field
        self._name: str = _name
        self._validation_method: Union[
            Literal[
                "STRICT", "IGNORE_MALFORMED", "COERCE"],
            ValidationMethod,
            None
        ] = validation_method
        self._ignore_unmapped: bool = ignore_unmapped
        return

    def _build(self) -> Dict:
        raise NotImplementedError("未实现Geo查询基类方法")

    def _build_point_inner_body(self) -> Dict:
        """
        构建点内部结构体
        :return:
        """
        return Bounds._build(self)

    def _build_points_inner_body(self) -> Dict:
        """
        构建多点内部结构体
        :return:
        """
        _body: Dict = {}
        if self._points:
            _body["points"] = [
                point._build()
                if isinstance(point, Point)
                else (
                    list(point)
                    if isinstance(point, Tuple)
                    else point
                )
                for point in self._points
            ]
        return _body

    def _build_outer_body(self, inner_body: Dict) -> Dict:
        """
        构建外层结构体
        :param inner_body:
        :return:
        """
        body: Dict = {
            self._field if isinstance(self._field, str) else self._field._field_name: inner_body
        }
        if self._name:
            body["_name"] = self._name
        if self._validation_method:
            body["validation_method"] = self._validation_method if isinstance(
                self._validation_method, str
            ) else self._validation_method.value
        return body


class GeoBoundingBox(_BaseGeoQueries):
    """
    地理边界框查询
    https://www.elastic.co/guide/en/elasticsearch/reference/7.17/query-dsl-geo-bounding-box-query.html

    Matches geo_point and geo_shape values that intersect a bounding box.
    匹配与边界框相交的 geo_point 和 geo_shape 值。

    :param field:
    :param _name:
        Optional name field to identify the filter
        用于标识筛选器的可选 name 字段
    :param validation_method:
        Set to IGNORE_MALFORMED to accept geo points with invalid latitude or longitude, set to COERCE to also try to
        infer correct latitude or longitude. (default is STRICT).
        设置为 IGNORE_MALFORMED 以接受纬度或经度无效的地理点，设置为 COERCE 以同时尝试推断正确的纬度或经度。（默认值为 STRICT）。
    :param ignore_unmapped:
        When set to true the ignore_unmapped option will ignore an unmapped field and will not match any documents for
        this query. This can be useful when querying multiple indexes which might have different mappings. When set to
        false (the default value) the query will throw an exception if the field is not mapped.
        当设置为 true 时，ignore_unmapped 选项将忽略未映射的字段，并且不会匹配此查询的任何文档。这在查询可能具有不同映射的多个索引时非常有用。
        当设置为 false （默认值） 时，如果字段未映射，则查询将引发异常。
    :param type_:
        [7.14.0] Deprecated in 7.14.0. This optional parameter is a no-op and has no effect on the query.
        [7.14.0] 在 7.14.0 中已废弃。此可选参数是 no-op，对 查询。
    :param top_left:
    :param bottom_right:
    :param top_right:
    :param bottom_left:
    :param topLeft:
    :param bottomRight:
    :param topRight:
    :param bottomLeft:
    :param top:
    :param left:
    :param bottom:
    :param right:
    """
    type: str = "geo_bounding_box"

    def __init__(
            self, field: Union[str, _BaseField], _name: str = None, ignore_unmapped: bool = None,
            validation_method: Union[Literal["STRICT", "IGNORE_MALFORMED", "COERCE"], ValidationMethod] = None,
            type_: str = None, top_left: PointType = None, bottom_right: PointType = None, top_right: PointType = None,
            bottom_left: PointType = None, topLeft: PointType = None, bottomRight: PointType = None,
            topRight: PointType = None, bottomLeft: PointType = None, top: PointType = None, left: PointType = None,
            bottom: PointType = None, right: PointType = None
    ):
        super().__init__(
            field=field, _name=_name, validation_method=validation_method, ignore_unmapped=ignore_unmapped, left=left,
            top_left=top_left, bottom_right=bottom_right, top_right=top_right, bottom_left=bottom_left, topLeft=topLeft,
            bottomRight=bottomRight, topRight=topRight, bottomLeft=bottomLeft, top=top, bottom=bottom, right=right,
        )
        self._type: str = type_
        return

    def _build(self) -> Dict:
        _body: Dict = self._build_point_inner_body()
        body: Dict = self._build_outer_body(inner_body=_body)
        if self._type:
            body["type"] = self._type
        return body


class GeoDistance(_BaseGeoQueries):
    """
    地理距离查询
    https://www.elastic.co/guide/en/elasticsearch/reference/7.17/query-dsl-geo-distance-query.html

    Matches geo_point and geo_shape values within a given distance of a geopoint.
    匹配地理点给定距离内的 geo_point 和 geo_shape 值。

    :param field:
    :param distance:
        The radius of the circle centred on the specified location. Points which fall into this circle are considered
        to be matches. The distance can be specified in various units. See Distance Units.
        以指定位置为中心的圆的半径。落入此圆圈的点被视为匹配项。距离可以用各种单位指定。请参阅 距离单位 。
    :param distance_type:
        How to compute the distance. Can either be arc (default), or plane (faster, but inaccurate on long distances
        and close to the poles).
        如何计算距离。可以是 arc （默认） 或 plane （更快，但在长距离和靠近极点时不准确）。
    :param _name:
        Optional name field to identify the filter
        用于标识筛选器的可选 name 字段
    :param validation_method:
        Set to IGNORE_MALFORMED to accept geo points with invalid latitude or longitude, set to COERCE to also try to
        infer correct latitude or longitude. (default is STRICT).
        设置为 IGNORE_MALFORMED 以接受纬度或经度无效的地理点，设置为 COERCE 以同时尝试推断正确的纬度或经度。（默认值为 STRICT）。
    :param ignore_unmapped:
        When set to true the ignore_unmapped option will ignore an unmapped field and will not match any documents for
        this query. This can be useful when querying multiple indexes which might have different mappings. When set to
        false (the default value) the query will throw an exception if the field is not mapped.
        当设置为 true 时，ignore_unmapped 选项将忽略未映射的字段，并且不会匹配此查询的任何文档。这在查询可能具有不同映射的多个索引时非常有用。
        当设置为 false （默认值） 时，如果字段未映射，则查询将引发异常。
    :param top_left:
    :param bottom_right:
    :param top_right:
    :param bottom_left:
    :param topLeft:
    :param bottomRight:
    :param topRight:
    :param bottomLeft:
    :param top:
    :param left:
    :param bottom:
    :param right:
    """
    type: str = "geo_distance"

    def __init__(
            self, field: Union[str, _BaseField], distance: Union[str, Number] = None, _name: str = None,
            ignore_unmapped: bool = None, distance_type: Union[Literal["arc", "plane"], DistanceType] = None,
            validation_method: Union[Literal["STRICT", "IGNORE_MALFORMED", "COERCE"], ValidationMethod] = None,
            top_left: PointType = None, bottom_right: PointType = None, top_right: PointType = None,
            bottom_left: PointType = None, topLeft: PointType = None, bottomRight: PointType = None,
            topRight: PointType = None, bottomLeft: PointType = None, top: PointType = None, left: PointType = None,
            bottom: PointType = None, right: PointType = None
    ):
        super().__init__(
            field=field, _name=_name, validation_method=validation_method, ignore_unmapped=ignore_unmapped, left=left,
            top_left=top_left, bottom_right=bottom_right, top_right=top_right, bottom_left=bottom_left, topLeft=topLeft,
            bottomRight=bottomRight, topRight=topRight, bottomLeft=bottomLeft, top=top, bottom=bottom, right=right,
        )
        self._distance: Union[str, Number] = distance
        self._distance_type: Union[Literal["arc", "plane"], DistanceType, None] = distance_type
        return

    def _build(self) -> Dict:
        _body: Dict = self._build_point_inner_body()
        body: Dict = self._build_outer_body(inner_body=_body)
        if self._distance:
            body["distance"] = self._distance
        if self._distance_type:
            body["distance_type"] = (
                self._distance_type if isinstance(self._distance_type, str) else self._distance_type.value
            )
        return body


class GeoPolygon(_BaseGeoQueries):
    """
    Geo-polygon 查询
    https://www.elastic.co/guide/en/elasticsearch/reference/7.17/query-dsl-geo-polygon-query.html

    Deprecated in 7.12.
    Use Geoshape instead where polygons are defined in GeoJSON or Well-Known Text (WKT).

    在 7.12 中已弃用。
    在 GeoJSON 或已知文本 （WKT） 中定义多边形的情况下，请改用 Geoshape。

    :param field:
    :param _name:
        Optional name field to identify the filter
        用于标识筛选器的可选 name 字段
    :param validation_method:
        Set to IGNORE_MALFORMED to accept geo points with invalid latitude or longitude, set to COERCE to also try to
        infer correct latitude or longitude. (default is STRICT).
        设置为 IGNORE_MALFORMED 以接受纬度或经度无效的地理点，设置为 COERCE 以同时尝试推断正确的纬度或经度。（默认值为 STRICT）。
    :param ignore_unmapped:
        When set to true the ignore_unmapped option will ignore an unmapped field and will not match any documents for
        this query. This can be useful when querying multiple indexes which might have different mappings. When set to
        false (the default value) the query will throw an exception if the field is not mapped.
        当设置为 true 时，ignore_unmapped 选项将忽略未映射的字段，并且不会匹配此查询的任何文档。这在查询可能具有不同映射的多个索引时非常有用。
        当设置为 false （默认值） 时，如果字段未映射，则查询将引发异常。
    :param points:
    """
    type: str = "geo_polygon"

    def __init__(
            self, field: Union[str, _BaseField], _name: str = None, ignore_unmapped: bool = None,
            validation_method: Union[Literal["STRICT", "IGNORE_MALFORMED", "COERCE"], ValidationMethod] = None,
            points: Iterable[PointType] = None
    ):
        super().__init__(field=field, _name=_name, validation_method=validation_method, ignore_unmapped=ignore_unmapped)
        self._points: Iterable[PointType] = points
        return

    def _build(self) -> Dict:
        _body: Dict = self._build_points_inner_body()
        body: Dict = self._build_outer_body(inner_body=_body)
        return body


class GeoShape(_BaseGeoQueries):
    """
    Geoshape 查询
    https://www.elastic.co/guide/en/elasticsearch/reference/7.17/query-dsl-geo-shape-query.html

    Filter documents indexed using the geo_shape or geo_point type.
    Requires the geo_shape mapping or the geo_point mapping.
    The geo_shape query uses the same grid square representation as the geo_shape mapping to find documents that have a
    shape that intersects with the query shape. It will also use the same Prefix Tree configuration as defined for the
    field mapping.
    The query supports two ways of defining the query shape, either by providing a whole shape definition, or by
    referencing the name of a shape pre-indexed in another index. Both formats are defined below with examples.

    筛选使用 geo_shape 或 geo_point 类型编制索引的文档。
    需要 geo_shape 映射或 geo_point 映射。
    geo_shape 查询使用与 geo_shape 映射相同的网格方形表示形式来查找其形状与查询形状相交的文档。它还将使用为字段映射定义的相同 Prefix Tree 配置。
    查询支持两种定义查询形状的方法，一种是提供完整的形状定义，另一种是引用在另一个索引中预先编制索引的形状的名称。下面通过示例定义了这两种格式。

    :param field:
    :param ignore_unmapped:
        When set to true the ignore_unmapped option will ignore an unmapped field and will not match any documents for
        this query. This can be useful when querying multiple indexes which might have different mappings. When set to
        false (the default value) the query will throw an exception if the field is not mapped.
        当设置为 true 时，ignore_unmapped 选项将忽略未映射的字段，并且不会匹配此查询的任何文档。这在查询可能具有不同映射的多个索引时非常有用。
        当设置为 false （默认值） 时，如果字段未映射，则查询将引发异常。
    :param shape:
        Similar to the geo_shape type, the geo_shape query uses GeoJSON to represent shapes.
        与 geo_shape 类型类似，geo_shape 查询使用 GeoJSON 来表示形状。
    :param indexed_shape:
        The query also supports using a shape which has already been indexed in another index. This is particularly
        useful for when you have a pre-defined list of shapes and you want to reference the list using a logical name
        (for example New Zealand) rather than having to provide coordinates each time. In this situation, it is only
        necessary to provide:
            - id - The ID of the document that containing the pre-indexed shape.
            - index - Name of the index where the pre-indexed shape is. Defaults to shapes.
            - path - The field specified as path containing the pre-indexed shape. Defaults to shape.
            - routing - The routing of the shape document if required.

        该查询还支持使用已在另一个索引中编制索引的形状。当您有一个预定义的形状列表，并且希望使用逻辑名称（例如 New Zealand）引用该列表，
        而不必每次都提供坐标时，这尤其有用。在这种情况下，只需提供：
            - id - 包含预先编制索引的形状的文档的 ID。
            - index - 预先编制索引的形状所在的索引的名称。默认为 shapes。
            - path - 指定为 path 的字段，其中包含预先编制索引的形状。默认为 shape。
            - routing - 形状文档的路由（如果需要）。
    :param relation:
    """
    type: str = "geo_shape"

    def __init__(
            self, field: Union[str, _BaseField], _name: str = None, ignore_unmapped: bool = None,
            shape: Dict = None, indexed_shape: Dict[Literal["id", "index", "path", "routing"], str] = None,
            relation: Union[Literal["INTERSECTS", "DISJOINT", "CONTAINS", "WITHIN"], SpatialRelation] = None
    ):
        super().__init__(field=field, _name=_name, ignore_unmapped=ignore_unmapped)
        self._shape: Dict = shape
        self._indexed_shape: Dict[Literal["id", "index", "path", "routing"], str] = indexed_shape
        self._relation: Union[Literal["INTERSECTS", "DISJOINT", "CONTAINS", "WITHIN"], SpatialRelation, None] = relation
        return

    def _build(self) -> Dict:
        _body: Dict = {}
        if self._shape is not None:
            _body["shape"] = self._shape
        if self._indexed_shape is not None:
            _body["indexed_shape"] = self._indexed_shape
        if self._relation is not None:
            _body["relation"] = self._relation.value
        return self._build_outer_body(inner_body=_body)










